// Copyright (c) 2019, the Dart project authors.  Please see the AUTHORS file
// for details. All rights reserved. Use of this source code is governed by a
// BSD-style license that can be found in the LICENSE file.

import "dart:convert";
import 'dart:typed_data';

import "package:test/test.dart";

import "package:package_config/package_config_types.dart";
import "package:package_config/src/packages_file.dart" as packages;
import "package:package_config/src/package_config_json.dart";
import "src/util.dart";

void throwError(Object error) => throw error;

void main() {
  group(".packages", () {
    test("valid", () {
      var packagesFile = "# Generated by pub yadda yadda\n"
          "foo:file:///foo/lib/\n"
          "bar:/bar/lib/\n"
          "baz:lib/\n";
      var result = packages.parse(utf8.encode(packagesFile),
          Uri.parse("file:///tmp/file.dart"), throwError);
      expect(result.version, 1);
      expect({for (var p in result.packages) p.name}, {"foo", "bar", "baz"});
      expect(result.resolve(pkg("foo", "foo.dart")),
          Uri.parse("file:///foo/lib/foo.dart"));
      expect(result.resolve(pkg("bar", "bar.dart")),
          Uri.parse("file:///bar/lib/bar.dart"));
      expect(result.resolve(pkg("baz", "baz.dart")),
          Uri.parse("file:///tmp/lib/baz.dart"));

      var foo = result["foo"];
      expect(foo, isNotNull);
      expect(foo.root, Uri.parse("file:///foo/"));
      expect(foo.packageUriRoot, Uri.parse("file:///foo/lib/"));
      expect(foo.languageVersion, LanguageVersion(2, 7));
    });

    test("valid empty", () {
      var packagesFile = "# Generated by pub yadda yadda\n";
      var result = packages.parse(
          utf8.encode(packagesFile), Uri.file("/tmp/file.dart"), throwError);
      expect(result.version, 1);
      expect({for (var p in result.packages) p.name}, <String>{});
    });

    group("invalid", () {
      var baseFile = Uri.file("/tmp/file.dart");
      void testThrows(String name, String content) {
        test(name, () {
          expect(
              () => packages.parse(utf8.encode(content), baseFile, throwError),
              throwsA(TypeMatcher<FormatException>()));
        });
        test(name + ", handle error", () {
          var hadError = false;
          packages.parse(utf8.encode(content), baseFile, (error) {
            hadError = true;
            expect(error, isA<FormatException>());
          });
          expect(hadError, true);
        });
      }

      testThrows("repeated package name", "foo:lib/\nfoo:lib\n");
      testThrows("no colon", "foo\n");
      testThrows("empty package name", ":lib/\n");
      testThrows("dot only package name", ".:lib/\n");
      testThrows("dot only package name", "..:lib/\n");
      testThrows("invalid package name character", "f\\o:lib/\n");
      testThrows("package URI", "foo:package:bar/lib/");
      testThrows("location with query", "f\\o:lib/?\n");
      testThrows("location with fragment", "f\\o:lib/#\n");
    });
  });

  group("package_config.json", () {
    test("valid", () {
      var packageConfigFile = """
        {
          "configVersion": 2,
          "packages": [
            {
              "name": "foo",
              "rootUri": "file:///foo/",
              "packageUri": "lib/",
              "languageVersion": "2.5",
              "nonstandard": true
            },
            {
              "name": "bar",
              "rootUri": "/bar/",
              "packageUri": "lib/",
              "languageVersion": "9999.9999"
            },
            {
              "name": "baz",
              "rootUri": "../",
              "packageUri": "lib/"
            },
            {
              "name": "noslash",
              "rootUri": "../noslash",
              "packageUri": "lib"
            }
          ],
          "generator": "pub",
          "other": [42]
        }
        """;
      var config = parsePackageConfigBytes(utf8.encode(packageConfigFile),
          Uri.parse("file:///tmp/.dart_tool/file.dart"), throwError);
      expect(config.version, 2);
      expect({for (var p in config.packages) p.name},
          {"foo", "bar", "baz", "noslash"});

      expect(config.resolve(pkg("foo", "foo.dart")),
          Uri.parse("file:///foo/lib/foo.dart"));
      expect(config.resolve(pkg("bar", "bar.dart")),
          Uri.parse("file:///bar/lib/bar.dart"));
      expect(config.resolve(pkg("baz", "baz.dart")),
          Uri.parse("file:///tmp/lib/baz.dart"));

      var foo = config["foo"];
      expect(foo, isNotNull);
      expect(foo.root, Uri.parse("file:///foo/"));
      expect(foo.packageUriRoot, Uri.parse("file:///foo/lib/"));
      expect(foo.languageVersion, LanguageVersion(2, 5));
      expect(foo.extraData, {"nonstandard": true});

      var bar = config["bar"];
      expect(bar, isNotNull);
      expect(bar.root, Uri.parse("file:///bar/"));
      expect(bar.packageUriRoot, Uri.parse("file:///bar/lib/"));
      expect(bar.languageVersion, LanguageVersion(9999, 9999));
      expect(bar.extraData, null);

      var baz = config["baz"];
      expect(baz, isNotNull);
      expect(baz.root, Uri.parse("file:///tmp/"));
      expect(baz.packageUriRoot, Uri.parse("file:///tmp/lib/"));
      expect(baz.languageVersion, null);

      // No slash after root or package root. One is inserted.
      var noslash = config["noslash"];
      expect(noslash, isNotNull);
      expect(noslash.root, Uri.parse("file:///tmp/noslash/"));
      expect(noslash.packageUriRoot, Uri.parse("file:///tmp/noslash/lib/"));
      expect(noslash.languageVersion, null);

      expect(config.extraData, {
        "generator": "pub",
        "other": [42]
      });
    });

    test("valid other order", () {
      // The ordering in the file is not important.
      var packageConfigFile = """
        {
          "generator": "pub",
          "other": [42],
          "packages": [
            {
              "languageVersion": "2.5",
              "packageUri": "lib/",
              "rootUri": "file:///foo/",
              "name": "foo"
            },
            {
              "packageUri": "lib/",
              "languageVersion": "9999.9999",
              "rootUri": "/bar/",
              "name": "bar"
            },
            {
              "packageUri": "lib/",
              "name": "baz",
              "rootUri": "../"
            }
          ],
          "configVersion": 2
        }
        """;
      var config = parsePackageConfigBytes(utf8.encode(packageConfigFile),
          Uri.parse("file:///tmp/.dart_tool/file.dart"), throwError);
      expect(config.version, 2);
      expect({for (var p in config.packages) p.name}, {"foo", "bar", "baz"});

      expect(config.resolve(pkg("foo", "foo.dart")),
          Uri.parse("file:///foo/lib/foo.dart"));
      expect(config.resolve(pkg("bar", "bar.dart")),
          Uri.parse("file:///bar/lib/bar.dart"));
      expect(config.resolve(pkg("baz", "baz.dart")),
          Uri.parse("file:///tmp/lib/baz.dart"));
      expect(config.extraData, {
        "generator": "pub",
        "other": [42]
      });
    });

    // Check that a few minimal configurations are valid.
    // These form the basis of invalid tests below.
    var cfg = '"configVersion":2';
    var pkgs = '"packages":[]';
    var name = '"name":"foo"';
    var root = '"rootUri":"/foo/"';
    test("minimal", () {
      var config = parsePackageConfigBytes(utf8.encode("{$cfg,$pkgs}"),
          Uri.parse("file:///tmp/.dart_tool/file.dart"), throwError);
      expect(config.version, 2);
      expect(config.packages, isEmpty);
    });
    test("minimal package", () {
      // A package must have a name and a rootUri, the remaining properties
      // are optional.
      var config = parsePackageConfigBytes(
          utf8.encode('{$cfg,"packages":[{$name,$root}]}'),
          Uri.parse("file:///tmp/.dart_tool/file.dart"),
          throwError);
      expect(config.version, 2);
      expect(config.packages.first.name, "foo");
    });

    test("nested packages", () {
      var configBytes = utf8.encode(json.encode({
        "configVersion": 2,
        "packages": [
          {"name": "foo", "rootUri": "/foo/", "packageUri": "lib/"},
          {"name": "bar", "rootUri": "/foo/bar/", "packageUri": "lib/"},
          {"name": "baz", "rootUri": "/foo/bar/baz/", "packageUri": "lib/"},
          {"name": "qux", "rootUri": "/foo/qux/", "packageUri": "lib/"},
        ]
      }));
      var config = parsePackageConfigBytes(configBytes,
          Uri.parse("file:///tmp/.dart_tool/file.dart"), throwError);
      expect(config.version, 2);
      expect(config.packageOf(Uri.parse("file:///foo/lala/lala.dart")).name,
          "foo");
      expect(
          config.packageOf(Uri.parse("file:///foo/bar/lala.dart")).name, "bar");
      expect(config.packageOf(Uri.parse("file:///foo/bar/baz/lala.dart")).name,
          "baz");
      expect(
          config.packageOf(Uri.parse("file:///foo/qux/lala.dart")).name, "qux");
      expect(config.toPackageUri(Uri.parse("file:///foo/lib/diz")),
          Uri.parse("package:foo/diz"));
      expect(config.toPackageUri(Uri.parse("file:///foo/bar/lib/diz")),
          Uri.parse("package:bar/diz"));
      expect(config.toPackageUri(Uri.parse("file:///foo/bar/baz/lib/diz")),
          Uri.parse("package:baz/diz"));
      expect(config.toPackageUri(Uri.parse("file:///foo/qux/lib/diz")),
          Uri.parse("package:qux/diz"));
    });

    group("invalid", () {
      void testThrows(String name, String source) {
        test(name, () {
          expect(
              () => parsePackageConfigBytes(utf8.encode(source),
                  Uri.parse("file:///tmp/.dart_tool/file.dart"), throwError),
              throwsA(TypeMatcher<FormatException>()));
        });
      }

      testThrows("comment", '# comment\n {$cfg,$pkgs}');
      testThrows(".packages file", 'foo:/foo\n');
      testThrows("no configVersion", '{$pkgs}');
      testThrows("no packages", '{$cfg}');
      group("config version:", () {
        testThrows("null", '{"configVersion":null,$pkgs}');
        testThrows("string", '{"configVersion":"2",$pkgs}');
        testThrows("array", '{"configVersion":[2],$pkgs}');
      });
      group("packages:", () {
        testThrows("null", '{$cfg,"packages":null}');
        testThrows("string", '{$cfg,"packages":"foo"}');
        testThrows("object", '{$cfg,"packages":{}}');
      });
      group("packages entry:", () {
        testThrows("null", '{$cfg,"packages":[null]}');
        testThrows("string", '{$cfg,"packages":["foo"]}');
        testThrows("array", '{$cfg,"packages":[[]]}');
      });
      group("package", () {
        testThrows("no name", '{$cfg,"packages":[{$root}]}');
        group("name:", () {
          testThrows("null", '{$cfg,"packages":[{"name":null,$root}]}');
          testThrows("num", '{$cfg,"packages":[{"name":1,$root}]}');
          testThrows("object", '{$cfg,"packages":[{"name":{},$root}]}');
          testThrows("empty", '{$cfg,"packages":[{"name":"",$root}]}');
          testThrows("one-dot", '{$cfg,"packages":[{"name":".",$root}]}');
          testThrows("two-dot", '{$cfg,"packages":[{"name":"..",$root}]}');
          testThrows(
              "invalid char '\\'", '{$cfg,"packages":[{"name":"\\",$root}]}');
          testThrows(
              "invalid char ':'", '{$cfg,"packages":[{"name":":",$root}]}');
          testThrows(
              "invalid char ' '", '{$cfg,"packages":[{"name":" ",$root}]}');
        });

        testThrows("no root", '{$cfg,"packages":[{$name}]}');
        group("root:", () {
          testThrows("null", '{$cfg,"packages":[{$name,"rootUri":null}]}');
          testThrows("num", '{$cfg,"packages":[{$name,"rootUri":1}]}');
          testThrows("object", '{$cfg,"packages":[{$name,"rootUri":{}}]}');
          testThrows("fragment", '{$cfg,"packages":[{$name,"rootUri":"x/#"}]}');
          testThrows("query", '{$cfg,"packages":[{$name,"rootUri":"x/?"}]}');
          testThrows("package-URI",
              '{$cfg,"packages":[{$name,"rootUri":"package:x/x/"}]}');
        });
        group("package-URI root:", () {
          testThrows(
              "null", '{$cfg,"packages":[{$name,$root,"packageUri":null}]}');
          testThrows("num", '{$cfg,"packages":[{$name,$root,"packageUri":1}]}');
          testThrows(
              "object", '{$cfg,"packages":[{$name,$root,"packageUri":{}}]}');
          testThrows("fragment",
              '{$cfg,"packages":[{$name,$root,"packageUri":"x/#"}]}');
          testThrows(
              "query", '{$cfg,"packages":[{$name,$root,"packageUri":"x/?"}]}');
          testThrows("package: URI",
              '{$cfg,"packages":[{$name,$root,"packageUri":"package:x/x/"}]}');
          testThrows("not inside root",
              '{$cfg,"packages":[{$name,$root,"packageUri":"../other/"}]}');
        });
        group("language version", () {
          testThrows("null",
              '{$cfg,"packages":[{$name,$root,"languageVersion":null}]}');
          testThrows(
              "num", '{$cfg,"packages":[{$name,$root,"languageVersion":1}]}');
          testThrows("object",
              '{$cfg,"packages":[{$name,$root,"languageVersion":{}}]}');
          testThrows("empty",
              '{$cfg,"packages":[{$name,$root,"languageVersion":""}]}');
          testThrows("non number.number",
              '{$cfg,"packages":[{$name,$root,"languageVersion":"x.1"}]}');
          testThrows("number.non number",
              '{$cfg,"packages":[{$name,$root,"languageVersion":"1.x"}]}');
          testThrows("non number",
              '{$cfg,"packages":[{$name,$root,"languageVersion":"x"}]}');
          testThrows("one number",
              '{$cfg,"packages":[{$name,$root,"languageVersion":"1"}]}');
          testThrows("three numbers",
              '{$cfg,"packages":[{$name,$root,"languageVersion":"1.2.3"}]}');
          testThrows("leading zero first",
              '{$cfg,"packages":[{$name,$root,"languageVersion":"01.1"}]}');
          testThrows("leading zero second",
              '{$cfg,"packages":[{$name,$root,"languageVersion":"1.01"}]}');
          testThrows("trailing-",
              '{$cfg,"packages":[{$name,$root,"languageVersion":"1.1-1"}]}');
          testThrows("trailing+",
              '{$cfg,"packages":[{$name,$root,"languageVersion":"1.1+1"}]}');
        });
      });
      testThrows("duplicate package name",
          '{$cfg,"packages":[{$name,$root},{$name,"rootUri":"/other/"}]}');
      testThrows("same roots",
          '{$cfg,"packages":[{$name,$root},{"name":"bar",$root}]}');
      testThrows(
          // The roots of foo and bar are the same.
          "same roots",
          '{$cfg,"packages":[{$name,$root},{"name":"bar",$root}]}');
      testThrows(
          // The root of bar is inside the root of foo,
          // but the package root of foo is inside the root of bar.
          "between root and lib",
          '{$cfg,"packages":['
              '{"name":"foo","rootUri":"/foo/","packageUri":"bar/lib/"},'
              '{"name":"bar","rootUri":"/foo/bar/"},"packageUri":"baz/lib"]}');
    });
  });

  group("factories", () {
    void testConfig(String name, PackageConfig config, PackageConfig expected) {
      group(name, () {
        test("structure", () {
          expect(config.version, expected.version);
          var expectedPackages = {for (var p in expected.packages) p.name};
          var actualPackages = {for (var p in config.packages) p.name};
          expect(actualPackages, expectedPackages);
        });
        for (var package in config.packages) {
          var name = package.name;
          test("package $name", () {
            var expectedPackage = expected[name];
            expect(expectedPackage, isNotNull);
            expect(package.root, expectedPackage.root, reason: "root");
            expect(package.packageUriRoot, expectedPackage.packageUriRoot,
                reason: "package root");
            expect(package.languageVersion, expectedPackage.languageVersion,
                reason: "languageVersion");
          });
        }
      });
    }

    var configText = """
     {"configVersion": 2, "packages": [
       {
         "name": "foo",
         "rootUri": "foo/",
         "packageUri": "bar/",
         "languageVersion": "1.2"
       }
     ]}
    """;
    var baseUri = Uri.parse("file:///start/");
    var config = PackageConfig([
      Package("foo", Uri.parse("file:///start/foo/"),
          packageUriRoot: Uri.parse("file:///start/foo/bar/"),
          languageVersion: LanguageVersion(1, 2))
    ]);
    testConfig(
        "string", PackageConfig.parseString(configText, baseUri), config);
    testConfig(
        "bytes",
        PackageConfig.parseBytes(
            Uint8List.fromList(configText.codeUnits), baseUri),
        config);
    testConfig("json", PackageConfig.parseJson(jsonDecode(configText), baseUri),
        config);

    baseUri = Uri.parse("file:///start2/");
    config = PackageConfig([
      Package("foo", Uri.parse("file:///start2/foo/"),
          packageUriRoot: Uri.parse("file:///start2/foo/bar/"),
          languageVersion: LanguageVersion(1, 2))
    ]);
    testConfig(
        "string2", PackageConfig.parseString(configText, baseUri), config);
    testConfig(
        "bytes2",
        PackageConfig.parseBytes(
            Uint8List.fromList(configText.codeUnits), baseUri),
        config);
    testConfig("json2",
        PackageConfig.parseJson(jsonDecode(configText), baseUri), config);
  });
}
